Meltdown & Spectre原理简要梳理

        Spectre以及meltdown漏洞是前段时间,十分热门的两个漏洞,它们之所以广受重视,是因为它们根据的是体系结构的设计漏洞,而非针对某个系统或者某个软件,因此它几乎可以遍及大多数近代的CPU。

这里主要有三个漏洞:

  • Variant 1: bounds check bypass (CVE-2017-5753)【绕过边界检查】
  • Variant 2: branch target injection (CVE-2017-5715)【分支目标注入】
  • Variant 3: rogue data cache load (CVE-2017-5754)【恶意数据缓存载入】
    Spectre 主要利用前两个漏洞进行攻击,而meltdown则主要利用第三个漏洞进行攻击。

一、内存映射

        Linux和Windows的内存映射方法是不同的,在linux中,虚拟空间地址有4G,0~3G为用户空间,3~4G为内核空间
虚拟空间

其中内核空间都相同,准确的讲是每个进程共享同一个内核空间
虚拟内存

        Linux在启动时会初始化一个进程,然后通过fork()生成子进程,Linux的fork机制会把父进程的页表和堆栈等一模一样地复制一份,然后在运行时,子进程通过缺页异常等操作来改变用户空间,如果内核空间部分也改变了,则只修改初始化进程的内核空间,其它子进程访问该页时,再通过缺页中断将这部分内容从父进程更新过来。

        一般来讲,当进程进行系统调用进入内核态的时候,它应该能够访问整个地址空间的,但是在这里只能访问自己的1G的地址空间,于是我们需要通过地址的映射,来使他可以访问整个空间,内核空间分为:ZONE_DMA(内存开始的16MB) 、ZONE_NORMAL(16MB~896MB)、ZONE_HIGHMEM(896MB ~ 结束)三个区域,其中0~896M是直接映射的,其余部分会进行非线性映射。

这里写图片描述

二、Tomasulo算法

tomasylo

        原始的Tomasulo算法是为了寄存器重命名以便消除指令的数据相关,数据一到就可以执行指令。

  1. 指令首先存在于指令队列之中。
  2. 指令一条一条地从队列中取出来,进行译码,然后放到对应的操作保留站中,比如加法指令放到加法保留站中。乘法指令放到乘除法保留站中。Vj和Vk中是用来存储源操作数的(已经就绪的源操作数值取自于浮点寄存器),若源操作数还没有准备好,则通过Qj和Qk指向操作数的保留站号,等到被指向的保留站中的操作计算出了结果,则结果可以通过结果总线传回到保留站中需要的指令,然后将其Qj和Qk中的值改为0(表示已经准备好)。
  3. 当Qj和Qk都为0时,那一条指令就可以送到乘/除器中执行。

三、乱序执行

        最早期 CPU都是顺序执行的,前一条指令未执行完毕,则后一条指令必须等待着,就像我们烧水,必须洗茶壶,烧水,洗茶杯,倒水必须按照顺序来做,但是事实上烧水和洗茶杯可以同时执行,其实有很多指令也是如此,调整它们的顺序可以加快程序执行的速度

可能会造成乱序执行的原因:

  1. 编译器为了优化而实现指令重排(静态调度)
  2. CPU实现指令的多发射,以及并行执行,并为了优化实现指令的重排(动态调度)

        程序的乱序执行并不意味着在所有步骤中,指令的顺序都是混乱的,事实上,指令在发射时和提交时依然是顺序的,只是在执行的过程中会打乱顺序。

为了支持乱序执行
带有ROB的Tomasulo算法

        我们在Tomasulo 算法中加入了ROB来使得提交结果的时候能够按照顺序提交,执行的结果暂时存放于ROB而不直接写入寄存器堆,然后再按顺序提交数据。从而可以不出问题。

本次的漏洞就是利用了乱序执行这一特性来实现的

四、分支预测

由于在指令中存在许多跳转和分支,为了提前访问分支中的代码以节约时间,我们加入了分支预测功能

静态分支

对于所有的跳转指令,我们都预测执行跳转或者执行不跳转,则称其为静态跳转

动态分支

动态分支将会通过历史跳转信息来预测下一次分支应该选择跳转还是选择不跳转。在intel设计中有一个称为BTB(Branch Target Buffer)的部件,当我们执行分支指令时,会将执行结果和分支指令地址记录在其中,当下次取址时,查询其中的记录,若存在,则根据历史执行记录进行预测是否跳转。

若不存在此记录,我们将会使用静态分支预测器,我们一般将向上跳转的分支指令看作循环,对于循环我们倾向于接受跳转,而对于向下跳转,我们倾向于不跳转。

五、Meltdown攻击

这里写图片描述
        在第一行,我们引发了一个异常,若程序按照顺序执行,第三行的access将不会被执行,但是由于程序是乱序执行的,因此在异常引发之前,第三行的access将被执行,但是执行结果存在ROB中不会被提交,当异常触发以后,第三行的执行结果将被撤销掉,从计算机外部看来第三行的access就跟从未被执行过一样,但是事实上它只是在后期被撤销了,这会带来一个小问题,就是在执行过程中,刚才读取的数据已经被存储到高速缓存中。

我们可以通过侧信道攻击来确认刚才访问的数据是什么。

这里写图片描述
这段代码揭示了攻击的关键部分
首先我们将访问内核地址并取内容到寄存器内,
若这一举动可以触发异常,则对应的寄存器清零,若异常未处理,则应用程序将会被终止,并且取出的值将会存入应用程序核心转储的寄存器中。我们要做的就是通过第5行将相应的寄存器清零,从而可以判断这是一个错误的值。然后再次重试,直到遇到一个不为0的值,然后将秘密的值作为地址,去做读取操作,以便cache记录到这个地址,实现侧信道攻击。

六、Spectre

这里写图片描述
        Spectre主要利用了分支预测和乱序执行的漏洞实现的,如图所示的代码,看起来十分地正常,若x小于array1的长度的时候,循环顺利执行。但是我们假设这里有一个存储密码的变量地址为secret,并且令a=secret-array1,于是我们可以使用array1[a]来表示secret的值。当多次执行循环的时候,我们的x满足循环条件,则我们的分支预测模块会认为下一个循环也满足循环条件而去预执行这个循环,若此时我们将a的值赋值给x,则分支预测模块预测本次循环为执行(其实并不会执行),CPU会预执行这个循环体,然后将我们存储密码的secret值取出来,并将其作为地址去访问array2,但是最终发现循环不应该被执行,于是刚才取出来的值将会被作废。但是我们的这个secret值的地址已经被存入到缓存中去。我们最终可以将array2读取一遍,若读取某个地址的时候,访问的时间特别短,则说明这个地址就是那个被存入缓存的地址(即我们的密码值)。

七、参考

https://googleprojectzero.blogspot.co.at/2018/01/reading-privileged-memory-with-side.html

https://lwn.net/Articles/738975/

http://lib.csdn.net/article/linux/29085

https://bugs.chromium.org/p/project-zero/issues/detail?id=1272

http://blog.csdn.net/yiyeguzhou100/article/details/72875122

https://bbs.pediy.com/thread-61327.htm

http://blog.csdn.net/muxiqingyang/article/details/6686738

Limin Wang wechat
Welcome!
I'm happy it's useful to you!
Show comments from Gitment